<?php

class GitTool
{
    public $gitBin = ''; //git bin文件绝对路径
    public $gitRepoUrl = ''; //git仓库地址

    public $srcPathRoot = ''; //存放git仓库的代码目录
    public $srcPath = ''; //某个git仓库存放的目录
    public $targetPathSymlink = ''; //网站的根目录, 是个软连接, 指向$targetPath
    public $targetPath = ''; //部署的实际目标目录
    public $targetCachePath = ''; //目标目录的缓存地址
    public $ignoreFiles = array(); //哪些文件是不需要被修改的
    public $cmd = []; //要执行的命令
    public $logPath = ''; //部署过程中记录日志的文件
    public $logInfo = array();

    public $startCommitId = ''; //最近一次commit id
    public $endCommitId = ''; //上次合并时最后一次commit id
    
    public $addedFileList = array(); //git pull 添加的文件
    public $changedFileList = array(); //git pull 修改的文件
    public $deletedFileList = array(); //git pull 删除的文件
    
    public $unknown = array(); //未知文件改动
    public $isShowResult = false; //是否用echo显示出部署结果信息
    public $eof = '<br>';
    
    //目标文件的权限信息
    public $group = 'www';
    public $user = 'www';
    public $mode = '700';
    public $modex= 0700;
    
    public function __construct()
    {
        $this->gitBin = GIT_BIN;
    }
    
    public function ini($configName)
    {
        $configFile = ROOT ."conf/{$configName}.php"; //配置文件
        if (file_exists($configFile) === FALSE) {
            echo "配置文件: {$configFile} 不存在".PHP_EOL;
        }
        include($configFile);
    
        $this->srcPathRoot       = REPO_DIR;
        $this->srcPath           = str_replace('\\', '/', REPO_DIR . $configName);
        $this->gitRepoUrl        = $config['git_repo_url'];
        $this->targetCachePath   = $config['cache_files'];
        $this->ignoreFiles       = $config['ignore_files'];
        $this->targetPath        = str_replace('\\', '/', $config['target_path']);
        $this->targetPathSymlink = str_replace('\\', '/', $config['target_path_symlink']);
        $this->cmd               = $config['cmd'];
    
        $str = date('Y-m-d H:i:s')." 开始部署: {$this->srcPath} -> {$this->targetPathSymlink}";
        $this->showResult($str);

        $this->imkdir($this->srcPath);

        //切换目录
        chdir($this->srcPath);

        //尝试检出最新代码
        $this->gitClone();
        
        return $this;
    }

    public function changeAuth($file='')
    {
        //chgrp($file, $this->group);
        //chown($file, $this->user);
        //chmod($file, $this->modex);

        $file = !empty($file) ? $file : $this->targetPath;
        if (is_dir($file)) {
            $this->execCmd("chown -R {$this->group}:{$this->user} $file");
            $this->execCmd("chmod -R {$this->mode} $file");

        } else {
            $this->execCmd("chown {$this->group}:{$this->user} $file");
            $this->execCmd("chmod {$this->mode} $file");
        }

        return $this;

    }

    //创建目录
    private function imkdir($dir)
    {
        if (file_exists($dir)) {
            return true;
        }
        //print_r(['创建目录: '.$dir]);
        mkdir($dir, $this->modex, true);
        //$this->changeAuth($dir);
    }

    //执行命名
    public function execCmd($cmd)
    {
        $return = '';
        exec($cmd, $output, $return);
        //print_r([$cmd, $output, $return]);
        $this->showResult($cmd);
        return $output;
    }

    //clone
    public function gitClone()
    {
        $this->imkdir($this->srcPath);
        
        if (file_exists($this->srcPath.'/.git')) {
            return $this;
        }
        $cmd = "$this->gitBin clone $this->gitRepoUrl .";
        $this->execCmd($cmd);
        return $this;
    }
    
    //分支列表
    public function branchList()
    {
        //拉取最新代码
        $cmd = "{$this->gitBin} fetch";
        $this->execCmd($cmd);
        
        $cmd = "{$this->gitBin} branch -a";
        return $this->execCmd($cmd);
    }

    //切换分支
    public function switchBranch($branchName='')
    {
        $branchName = !empty($branchName) ? $branchName : 'master';
        $cmd = "{$this->gitBin} checkout {$branchName}";
        $this->execCmd($cmd);

        return $this;
    }
    
    //镜像列表
    public function tagList()
    {
        //拉取最新代码
        $cmd = "{$this->gitBin} fetch";
        $this->execCmd($cmd);
    
        //拉取所有tag
        $cmd = "{$this->gitBin} tag";
        return $this->execCmd($cmd);
    }

    //切换镜像
    public function switchTag($tagName='')
    {
        //找到要更新的tag
        if (empty($tagName)) {
            $tagList = $this->tagList();
            $tagName = array_pop($tagList);
        }

        //切换tag
        $cmd = "{$this->gitBin} checkout {$tagName}";
        $this->execCmd($cmd);

        return $this;
    }
    
	//拉取最新代码
    public function gitPull()
    {
        //拉取最新代码
        $output = [];
        $command = "{$this->gitBin} pull";
        $output = $this->execCmd($command);
        if (end($output) == 'Already up-to-date.') {
            $this->showResult('已是最新.');
        } elseif (strpos($output[0], 'Updating') !== FALSE) {
            preg_match('/Updating\s([a-z0-9]+)\.\.([a-z0-9]+)/', $output[0], $matches);
            if (!empty($matches[1]) && !empty($matches[2])) {
                $this->startCommitId = $matches[1];
                $this->endCommitId = $matches[2];
                //$this->showResult($matches[0]);
            } else {
                $this->showResult('解析失败1.');
            }
        } elseif (strpos($output[0], '更新') !== FALSE) {
            preg_match('/更新\s([a-z0-9]+)\.\.([a-z0-9]+)/', $output[0], $matches);
            if (!empty($matches[1]) && !empty($matches[2])) {
                $this->startCommitId = $matches[1];
                $this->endCommitId = $matches[2];
            } else {
                $this->showResult('解析失败2.');
            }
        } else {
            $this->showResult($output);
        }
        
        return $this;
    }

    //获取差异
    public function gitDiff()
    {
        if (empty($this->startCommitId) || empty($this->endCommitId)) {
            $this->showResult('为获取到最近两次提交id');
            return $this;
        }

        $command = "{$this->gitBin} diff --name-status {$this->startCommitId} {$this->endCommitId}";
        $output = $this->execCmd($command);

        foreach ($output as $v) {
            $arr = explode("\t", $v);
            $path = $this->srcPath .'/'. $arr[1];
            switch ($arr[0]) {
                case 'A':
                    $this->addedFileList[] = $path;
                    break;
                case 'M':
                    $this->changedFileList[] = $path;
                    break;
                case 'D':
                    $this->deletedFileList[] = $path;
                    break;
                default:
                    $this->unknown[] = $v;
                    break;
            }
        }
                    
        if (!empty($this->unknown)) {
            $this->showResult('未知改动');
            $this->showResult($this->unknown);
        }
        
        return $this;
    }

	//将有改动的文件同步到目标目录
    public function deployPart()
    {
        $this->gitPull()->gitDiff(); //获取变化

        $result = array(); //记录结果
        
        //添加目标文件
        foreach ($this->addedFileList as $srcFile) {
            $targetFile = str_replace($this->srcPath, $this->targetPathSymlink, $srcFile);
            
            if ($this->isIgnoreFile($srcFile)) {
                $result[] = "{$targetFile} 添加失败, 忽略文件, 不能添加.";
                continue;
            }
            
            $targetFileDir = dirname($targetFile);
            if (!file_exists($targetFileDir)) {
                $this->imkdir($targetFileDir); //创建目录
            }

            $status = copy($srcFile, $targetFile);
        
            if ($status) {
                //$this->changeAuth($targetFile);
                $result[] = "{$targetFile} 添加成功.";
            } else {
                $result[] = "{$targetFile} 添加失败.";
            }
        }

        //修改目标文件
        foreach ($this->changedFileList as $srcFile) {
            $targetFile = str_replace($this->srcPath, $this->targetPathSymlink, $srcFile);
            if ($this->isIgnoreFile($srcFile)) {
                $result[] = "{$targetFile} 修改失败, 忽略文件, 不能修改.";
                continue;
            }
        
            $targetFileDir = dirname($targetFile);
            if (!file_exists($targetFileDir)) {
                $this->imkdir($targetFileDir); //创建目录
            }

            $status = copy($srcFile, $targetFile);

            if ($status) {
                //$this->changeAuth($targetFile);
                $result[] = "{$targetFile} 修改成功.";
            } else {
                $result[] = "{$targetFile} 修改失败.";
            }
        
        }

        //删除目标文件
        foreach ($this->deletedFileList as $srcFile) {
            $targetFile = str_replace($this->srcPath, $this->targetPathSymlink, $srcFile);
            if ($this->isIgnoreFile($srcFile)) {
                $result[] = "{$targetFile} 删除失败, 此文件是忽略文件, 不能删除.";
                continue;
            }
        
            if (file_exists($targetFile)) {
                $status = unlink($targetFile);
                if ($status) {
                    $result[] = "{$targetFile} 删除成功.";
                } else {
                    $result[] = "{$targetFile} 删除失败.";
                }
            } else {
                $result[] = "{$targetFile} 文件不存在, 删除成功.";
            }
        }
    
        if (empty($result)) {
            $this->showResult('本次部署没有文件发生变化.');
        } else {
            $this->showResult($result);
        }
    
        return $this;
    }
    
    //除了忽略的文件, 全部复制到目标目录
    public function deployAll()
    {
        $result = array(); //记录结果
    
        $command = "{$this->gitBin} pull";
        $this->execCmd($command);
        
        //获取所有文件
        $srcFiles = Dir::ini($this->srcPath)->fileList;
    
        //创建新的代码目录, 再创建软连接
        $this->targetPath .= '/'.date('Ymd_His');
        $this->imkdir($this->targetPath);
        
        foreach ($srcFiles as $srcFile) {
            $targetFile = str_replace($this->srcPath, $this->targetPath, $srcFile);
            
            if ($this->isIgnoreFile($srcFile)) {
                continue;
            }
            
            //创建目录
            $arrTargetFile = explode('/', $targetFile);
            array_pop($arrTargetFile);
            $this->imkdir(implode('/', $arrTargetFile));
    
            $bool = copy($srcFile, $targetFile);
            if ($bool) {
                //$this->changeAuth($targetFile);
                $result[] = "$targetFile 复制成功.";
            } else {
                $result[] = "$targetFile 复制失败.";
            }
        }
        
        //file_exists($this->targetPathSymlink) && unlink($this->targetPathSymlink);
        unlink($this->targetPathSymlink);
        symlink($this->targetPath, $this->targetPathSymlink); //创建软连接

        $this->execCmd("chown -h {$this->group}:{$this->user} {$this->targetPathSymlink} ");
    
        $this->showResult($result);
        
        return $this;
    }
    
	//清除目标目录的缓存文件夹
    public function clearCache()
    {
        if (is_string($this->targetCachePath)) {
            $this->targetCachePath = array($this->targetCachePath);
        }
        
        foreach ($this->targetCachePath as $cache) {
            if (!empty($cache) && file_exists($cache)) {
                if (is_dir($cache)) {
                    $cacheList = Dir::ini($cache)->fileList;
                    foreach ($cacheList as $file) {
                        unlink($file);
                    }

                } else {
                    unlink($cache);
                }
                $this->showResult('删除缓存：'.$cache);
            }
        }
    
        return $this;
    }

    public function cmd()
    {
        $replace = [
            '{target_path}' => $this->targetPath
        ];
        foreach ($this->cmd as $cmd) {
            $cmd = str_replace(array_keys($replace), $replace, $cmd);
            $this->execCmd($cmd);
        }

        return $this;
    }
    
    //收尾的一些操作
    public function over()
    {
        $this->srcPath = '';
        $this->targetPath = '';
        $this->targetCachePath = '';
        $this->ignoreFiles = array();
        $this->logInfo = array();
        
        $this->startCommitId = ''; //最近一次commit id
        $this->endCommitId = ''; //上次合并时最后一次commit id
        
        $this->addedFileList = array(); //git pull 添加的文件
        $this->changedFileList = array(); //git pull 修改的文件
        $this->deletedFileList = array(); //git pull 删除的文件
        
        $this->unknown = array(); //未知文件改动
        $this->isShowResult = false; //是否用echo显示出部署结果信息
    }
    
    //判断源文件是不是被忽略的
    public function isIgnoreFile($file)
    {
        $file = str_replace('\\', '/', $file);
        foreach ($this->ignoreFiles as $ignore) {
            if (strpos($file, $ignore) !== FALSE) {
                return TRUE;
            }
        }
        
        return FALSE;
    }
    
    function filelog($text, $isWrite = FALSE)
    {
        if (!is_string($text)) {
            $text = json_encode($text);
        }
        
        if ($isWrite == FALSE) {
            $this->logInfo[] = $text;
        } else {
            $this->logInfo[] = $text;
            $str = implode(PHP_EOL, $this->logInfo);
            $this->logInfo = array();
            file_put_contents($this->logPath, $str.PHP_EOL.PHP_EOL, FILE_APPEND);
        }
    }
    
    public function showResult($arr)
    {
        if ($this->isShowResult){
            if (is_string($arr)) {
                $arr = [$arr];
            }
            echo $this->eof. implode($this->eof, $arr). $this->eof;
        }
    }

    private function isWin()
    {
        if (strpos(PHP_OS, 'WIN') !== FALSE) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

}